﻿Imports System.Windows.Forms
Imports System.Diagnostics
Imports System.Collections
Imports Microsoft.Win32
Imports System.IO
Imports System.Text
Imports System.Runtime.InteropServices

' MRI list manager.
'
' Written by: Alex Farber
'
' alexm@cmt.co.il

'******************************************************************************
'
'Using:
'
'1) Add menu item Recent Files (or any name you want) to main application menu.
'   This item is used by MRUManager as popup menu for MRU list.
'
'2) Implement IMRUClient inteface in the form class:
'
'public class Form1 : System.Windows.Forms.Form, IMRUClient
'{
'     public void OpenMRUFile(string fileName)
'     {
'         // open file here
'     }
' 
'     // ...    
'}
'
'3) Add MRUManager member to the form class and initialize it:
'
'     private MRUManager mruManager;
'
'     private void Form1_Load(object sender, System.EventArgs e)
'     {
'         mruManager = new MRUManager();
'         mruManager.Initialize(
'             this,                              // owner form
'             mnuFileMRU,                        // Recent Files menu item
'             "Software\\MyCompany\\MyProgram"); // Registry path to keep MRU list
'
'        // Optional. Call these functions to change default values:
'
'        mruManager.CurrentDir = ".....";           // default is current directory
'        mruManager.MaxMRULength = ...;             // default is 10
'        mruMamager.MaxDisplayNameLength = ...;     // default is 40
'     }
'
'     NOTES:
'     - If Registry path is, for example, "Software\MyCompany\MyProgram",
'       MRU list is kept in
'       HKEY_CURRENT_USER\Software\MyCompany\MyProgram\MRU Registry entry.
'
'     - CurrentDir is used to show file names in the menu. If file is in
'       this directory, only file name is shown.
'
'4) Call MRUManager Add and Remove functions when necessary:
'
'       mruManager.Add(fileName);          // when file is successfully opened
'
'       mruManager.Remove(fileName);       // when Open File operation failed
'
'******************************************************************************


' Implementation details:
'
' MRUManager loads MRU list from Registry in Initialize function.
' List is saved in Registry when owner form is closed.
'
' MRU list in the menu is updated when parent menu is poped-up.
'
' Owner form OpenMRUFile function is called when user selects file
' from MRU list.




Namespace MRU

    ''' <summary>
    ''' Interface which should be implemented by owner form
    ''' to use MRUManager.
    ''' </summary>
    Public Interface IMRUClient
        Sub OpenMRUFile(ByVal fileName As String)
    End Interface

    ''' <summary>
    ''' MRU manager - manages Most Recently Used Files list
    ''' for Windows Form application.
    ''' </summary>
    Public Class MRUManager
#Region "Members"

        Private ownerForm As Form
        ' owner form

        Private menuItemParent As ToolStripMenuItem
        ' Recent Files menu item parent
        Private registryPath As String
        ' Registry path to keep MRU list
        Private maxNumberOfFiles As Integer = 10
        ' maximum number of files in MRU list
        Private maxDisplayLength As Integer = 40
        ' maximum length of file name for display
        Private currentDirectory As String
        ' current directory
        Private mruList As ArrayList
        ' MRU list (file names)
        Private Const regEntryName As String = "file"
        ' entry name to keep MRU (file0, file1...)

#End Region

#Region "Windows API"

        ' BOOL PathCompactPathEx(          
        '    LPTSTR pszOut,
        '    LPCTSTR pszSrc,
        '    UINT cchMax,
        '    DWORD dwFlags
        '    );

        <DllImport("shlwapi.dll", CharSet:=CharSet.Auto)> _
        Private Shared Function PathCompactPathEx(ByVal pszOut As StringBuilder, ByVal pszPath As String, ByVal cchMax As Integer, ByVal reserved As Integer) As Boolean
        End Function

#End Region

#Region "Constructor"

        Public Sub New()
            mruList = New ArrayList()
        End Sub

#End Region

#Region "Public Properties"

        ''' <summary>
        ''' Maximum length of displayed file name in menu (default is 40).
        ''' 
        ''' Set this property to change default value (optional).
        ''' </summary>
        Public Property MaxDisplayNameLength() As Integer


            Get
                Return maxDisplayLength
            End Get
            Set(ByVal value As Integer)
                maxDisplayLength = value
                If maxDisplayLength < 10 Then
                    maxDisplayLength = 10
                End If
            End Set
        End Property

        ''' <summary>
        ''' Maximum length of MRU list (default is 10).
        ''' 
        ''' Set this property to change default value (optional).
        ''' </summary>
        Public Property MaxMRULength() As Integer

            Get
                Return maxNumberOfFiles
            End Get
            Set(ByVal value As Integer)
                maxNumberOfFiles = value
                If maxNumberOfFiles < 1 Then
                    maxNumberOfFiles = 1
                End If
                If mruList.Count > maxNumberOfFiles Then
                    mruList.RemoveRange(maxNumberOfFiles - 1, mruList.Count - maxNumberOfFiles)
                End If
            End Set
        End Property

        ''' <summary>
        ''' Set current directory.
        ''' 
        ''' Default value is program current directory which is set when
        ''' Initialize function is called.
        ''' 
        ''' Set this property to change default value (optional)
        ''' after call to Initialize.
        ''' </summary>
        Public Property CurrentDir() As String

            Get
                Return currentDirectory
            End Get
            Set(ByVal value As String)
                currentDirectory = value
            End Set
        End Property

#End Region

#Region "Public Functions"

        ''' <summary>
        ''' Initialization. Call this function in form Load handler.
        ''' </summary>
        ''' <param name="owner">Owner form</param>
        ''' <param name="mruItem">Recent Files menu item</param>
        ''' <param name="regPath">Registry Path to keep MRU list</param>
        Public Sub Initialize(ByVal owner As Form, _
                                ByVal mruItem As ToolStripMenuItem, ByVal regPath As String)
            ' keep reference to owner form
            ownerForm = owner

            ' check if owner form implements IMRUClient interface
            If Not (TypeOf owner Is IMRUClient) Then
                Throw New Exception("MRUManager: Owner form doesn't implement IMRUClient interface")
            End If

            ' keep reference to MRU menu item parent
            Me.menuItemParent = mruItem

            If menuItemParent Is Nothing Then
                Throw New Exception("MRUManager: Cannot find parent of MRU menu item")
            End If

            ' keep Registry path adding MRU key to it
            registryPath = regPath
            If registryPath.EndsWith("\") Then
                registryPath += "MRU"
            Else
                registryPath += "\MRU"
            End If


            ' keep current directory in the time of initialization
            currentDirectory = Directory.GetCurrentDirectory()

            '' subscribe to MRU parent Popup event
            'AddHandler menuItemParent.DropDownOpened, New EventHandler(AddressOf Me.OnMRUParentPopup)

            ' subscribe to owner form Closing event
            AddHandler ownerForm.Closing, AddressOf OnOwnerClosing

            ' load MRU list from Registry
            Me.loadMRU()
            Me.buildMRUMenuItem()
        End Sub

        ''' <summary>
        ''' Add file name to MRU list.
        ''' Call this function when file is opened successfully.
        ''' If file already exists in the list, it is moved to the first place.
        ''' </summary>
        ''' <param name="file">File Name</param>
        Public Sub AddMRUMenuItem(ByVal file As String)
            Remove(file)

            ' if array has maximum length, remove last element
            If mruList.Count = maxNumberOfFiles Then
                mruList.RemoveAt(maxNumberOfFiles - 1)
            End If

            ' add new file name to the start of array
            mruList.Insert(0, file)
        End Sub

        ''' <summary>
        ''' Remove file name from MRU list.
        ''' Call this function when File - Open operation failed.
        ''' </summary>
        ''' <param name="file">File Name</param>
        Public Sub Remove(ByVal file As String)
            Dim i As Integer = 0

            Dim myEnumerator As IEnumerator = mruList.GetEnumerator()

            While myEnumerator.MoveNext()
                If DirectCast(myEnumerator.Current, String) = file Then
                    mruList.RemoveAt(i)
                    Return
                End If

                i += 1
            End While
        End Sub

#End Region

#Region "Event Handlers"

        ''' <summary>
        ''' Update MRU list when MRU menu item parent is opened
        ''' </summary>
        Private Sub buildMRUMenuItem()
            ' remove all children

            Me.menuItemParent.DropDownItems.Clear()

            ' Disable menu item if MRU list is empty
            If mruList.Count = 0 Then
                Me.menuItemParent.Enabled = False
                Return
            End If

            ' enable menu item and add child items
            Me.menuItemParent.Enabled = True

            Dim item As ToolStripMenuItem

            Dim myEnumerator As IEnumerator = mruList.GetEnumerator()

            While myEnumerator.MoveNext()
                Dim lItemName As String = DirectCast(myEnumerator.Current, String)
                Dim lItemDisplay As String = GetDisplayName(lItemName)

                item = New ToolStripMenuItem(lItemDisplay)
                If lItemDisplay <> lItemName Then
                    item.ToolTipText = lItemName
                End If
                item.Tag = lItemName
                ' subscribe to item's Click event
                AddHandler item.Click, AddressOf Me.OnMRUClicked
                Me.menuItemParent.DropDownItems.Add(item)
            End While

        End Sub

        ''' <summary>
        ''' MRU menu item is clicked - call owner's OpenMRUFile function
        ''' </summary>
        ''' <param name="sender"></param>
        ''' <param name="e"></param>
        Private Sub OnMRUClicked(ByVal sender As Object, ByVal e As EventArgs)
            Dim sItem As String

            Try
                ' cast sender object to MenuItem
                Dim item As ToolStripMenuItem = DirectCast(sender, ToolStripMenuItem)

                If item IsNot Nothing Then
                    ' Get file name from list using item index
                    sItem = DirectCast(mruList(GetMenuItemIndexByTag(item)), String)

                    ' call owner's OpenMRUFile function
                    If sItem.Length > 0 Then
                        DirectCast(ownerForm, IMRUClient).OpenMRUFile(sItem)
                    End If

                End If

            Catch ex As Exception
                Trace.WriteLine("Exception in OnMRUClicked: " & ex.Message)
            End Try
        End Sub

        Public Function GetMenuItemIndexByTag(ByVal oMenuItem As System.Windows.Forms.ToolStripMenuItem) As Integer

            Dim parentMenu As ToolStrip = oMenuItem.Owner
            For index As Integer = 0 To parentMenu.Items.Count - 1
                If CStr(oMenuItem.Tag) = CStr(parentMenu.Items(index).Tag) Then
                    Return index
                End If
            Next

            
            Return -1
        End Function

        ''' <summary>
        ''' Save MRU list in Registry when owner form is closing
        ''' </summary>
        ''' <param name="sender"></param>
        ''' <param name="e"></param>
        Private Sub OnOwnerClosing(ByVal sender As Object, ByVal e As System.ComponentModel.CancelEventArgs)
            Dim i As Integer, n As Integer

            Try
                Dim key As RegistryKey = Registry.CurrentUser.CreateSubKey(registryPath)

                If key IsNot Nothing Then
                    n = mruList.Count

                    For i = 0 To maxNumberOfFiles - 1
                        key.DeleteValue(regEntryName & i.ToString(), False)
                    Next

                    For i = 0 To n - 1
                        key.SetValue(regEntryName & i.ToString(), mruList(i))
                    Next

                End If
            Catch ex As Exception
                Trace.WriteLine("Saving MRU to Registry failed: " & ex.Message)
            End Try
        End Sub


#End Region

#Region "Private Functions"

        ''' <summary>
        ''' Load MRU list from Registry.
        ''' Called from Initialize.
        ''' </summary>
        Private Sub loadMRU()
            Dim sKey As String, s As String
            mruList.Clear()

            Dim key As RegistryKey = Registry.CurrentUser.OpenSubKey(registryPath)

            If key IsNot Nothing Then
                For i As Integer = 0 To maxNumberOfFiles - 1
                    sKey = regEntryName & i.ToString()

                    s = DirectCast(key.GetValue(sKey, String.Empty), String)

                    If s.Length = 0 Then
                        Exit For
                    End If

                    mruList.Add(s)
                Next
            End If

        End Sub

        ''' <summary>
        ''' Get display file name from full name.
        ''' </summary>
        ''' <param name="fullName">Full file name</param>
        ''' <returns>Short display name</returns>
        Private Function GetDisplayName(ByVal fullName As String) As String
            ' if file is in current directory, show only file name
            Dim fileInfo As New FileInfo(fullName)

            If fileInfo.DirectoryName = currentDirectory Then
                Return GetShortDisplayName(fileInfo.Name, maxDisplayLength)
            End If

            Return GetShortDisplayName(fullName, maxDisplayLength)
        End Function

        ''' <summary>
        ''' Truncate a path to fit within a certain number of characters 
        ''' by replacing path components with ellipses.
        ''' 
        ''' This solution is provided by CodeProject and GotDotNet C# expert
        ''' Richard Deeming.
        ''' 
        ''' </summary>
        ''' <param name="longName">Long file name</param>
        ''' <param name="maxLen">Maximum length</param>
        ''' <returns>Truncated file name</returns>
        Private Function GetShortDisplayName(ByVal longName As String, ByVal maxLen As Integer) As String
            Dim pszOut As New StringBuilder(maxLen + maxLen + 2)
            ' for safety
            If PathCompactPathEx(pszOut, longName, maxLen, 0) Then
                Return pszOut.ToString()
            Else
                Return longName
            End If
        End Function

#End Region

    End Class
End Namespace
